sartUP — Contenitore Admin “menu–oriented” (Blade + Auth nativa + Spatie ruoli)
sartUP — Contenitore Admin “menu–oriented” (Blade + Auth nativa + Spatie ruoli)
Obiettivo: tutto in Markdown per Cursor: struttura cartelle coerente con il menù, controllers/models/views/helpers organizzati per sezioni del menù, con Service Menu (Super Admin) in testa e Configurazione Menù come prima voce.---
0) Filosofia di struttura (menu → cartelle)
L’architettura del codice ricalca la gerarchia del menù:- Topbar (Livello 1) = macro–sezione (es. Dashboard, Industria 4.0, Impostazioni, Servizio).
- Sidebar (Livello 2/3/4) = sottosezioni/nodi figli.
- Il codice va in sottoCartelle parlanti che ripetono i nomi delle sezioni.
- Le rotte sono raggruppate per sezione, i controller sono in namespace dedicati, le view Blade seguono la stessa tassonomia.
- Super Admin: bypass via
- Ruolo attivo in sessione:
- Menu filtering:
- Audit (futuro): log policy per CRUD delle voci menu.
- Login/logout/reset funzionanti.
- Selettore ruolo attivo post-login (+ badge ruolo in header).
- Topbar & Sidebar generati da DB (MenuService).
- Servizio (super-admin) in topbar; prima voce = Configurazione menù CRUD funzionante.
- Voce Industria 4.0 → Report → Macchine → Elenco macchine collegate presente (placeholder).
- [ ] Creare
- [ ] Implementare controller come da namespace/cartelle
- [ ] Migrazioni e seeders (Menu, MenuItem, Ruoli, SuperAdmin) con “Servizio”
- [ ]
- [ ] Layout Blade
- [ ] Middleware
- [ ] Gate super-admin in
- [ ] Seed “Servizio” con “Configurazione menù” come prima voce
---
1) Struttura cartelle (proposta)
``
app/
Http/
Controllers/
Admin/
Dashboard/
DashboardController.php
I40/
HomeController.php
Machines/
MachinesController.php
Systems/ # "Servizio" (super-admin only)
Menu/
MenuController.php
Users/
UsersController.php
Roles/
RolesController.php
Middleware/
EnsureActiveRole.php
Models/
Menu.php
MenuItem.php
Policies/
MenuPolicy.php
app/
Services/
MenuService.php
resources/
views/
layouts/
admin.blade.php
auth.blade.php
auth/
login.blade.php
select-role.blade.php
forgot.blade.php
reset.blade.php
admin/
dashboard/index.blade.php
i40/home.blade.php
i40/machines/connected.blade.php
systems/ # Servizio (Super Admin)
index.blade.php
menu/
index.blade.php # lista voci
create.blade.php
edit.blade.php
routes/
admin.php
auth.php
database/
migrations/
2025_xx_xx_create_menus_table.php
2025_xx_xx_create_menu_items_table.php
seeders/
RolesSeeder.php
SuperAdminSeeder.php
MenuSeeder.php
`> Nota: Systems ≈ Servizio. La voce di topbar “Servizio” sarà visibile solo al ruolo
super-admin.---
2) Rotte (separate per ambito)
2.1
routes/auth.php
`php
<?phpuse Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\ResetPasswordController;
use App\Http\Controllers\Auth\RoleSelectorController;
Route::middleware('guest')->group(function () {
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('/login', [LoginController::class, 'login'])->name('login.post');
Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update');
});
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');
Route::middleware('auth')->group(function () {
Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select');
Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set');
});
`2.2
routes/admin.php
`php
<?phpuse Illuminate\Support\Facades\Route;
Route::middleware(['auth','active.role'])->prefix('admin')->name('admin.')->group(function () {
// Dashboard
Route::get('/', [\App\Http\Controllers\Admin\Dashboard\DashboardController::class,'index'])
->name('dashboard');
// Industria 4.0
Route::prefix('i40')->name('i40.')->group(function() {
Route::get('/', [\App\Http\Controllers\Admin\I40\HomeController::class,'index'])->name('home');
Route::get('/machines/connected', [\App\Http\Controllers\Admin\I40\Machines\MachinesController::class,'connected'])
->name('machines.connected');
});
// Servizio (Super Admin menu)
Route::prefix('systems')->name('systems.')->middleware('role:super-admin')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('home');
// Configurazione menù (prima voce)
Route::prefix('menu')->name('menu.')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('index');
Route::get('/create', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'create'])->name('create');
Route::post('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'store'])->name('store');
Route::get('/{item}/edit', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'edit'])->name('edit');
Route::put('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'update'])->name('update');
Route::delete('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'destroy'])->name('destroy');
Route::post('/reorder', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'reorder'])->name('reorder');
});
});
});
`---
3) Controller — esempi orientati a cartelle
3.1 Dashboard
`php
<?php
namespace App\Http\Controllers\Admin\Dashboard;use App\Http\Controllers\Controller;
class DashboardController extends Controller
{
public function index() {
return view('admin.dashboard.index');
}
}
`3.2 I4.0 → Machines
`php
<?php
namespace App\Http\Controllers\Admin\I40\Machines;use App\Http\Controllers\Controller;
class MachinesController extends Controller
{
public function connected() {
return view('admin.i40.machines.connected');
}
}
`3.3 Systems → Menu (super-admin)
`php
<?php
namespace App\Http\Controllers\Admin\Systems\Menu;use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Menu;
use App\Models\MenuItem;
class MenuController extends Controller
{
public function index() {
$menu = Menu::where('name','admin_main')->first();
$items = $menu ? $menu->items()->orderBy('order_index')->get() : collect();
return view('admin.systems.menu.index', compact('menu','items'));
}
public function create() {
$menu = Menu::where('name','admin_main')->first();
$parents = $menu?->items()->orderBy('label')->get();
return view('admin.systems.menu.create', compact('menu','parents'));
}
public function store(Request $r) {
$r->validate([
'label'=>'required|string|max:100',
'parent_id'=>'nullable|integer',
'route_name'=>'nullable|string|max:120',
'url'=>'nullable|url',
'order_index'=>'nullable|integer',
]);
$menu = Menu::firstOrCreate(['name'=>'admin_main']);
MenuItem::create([
'menu_id'=>$menu->id,
'parent_id'=>$r->parent_id,
'label'=>$r->label,
'route_name'=>$r->route_name ?: null,
'url'=>$r->url ?: null,
'icon'=>$r->icon,
'description'=>$r->description,
'order_index'=>$r->order_index ?? 0,
'is_visible'=>$r->boolean('is_visible', true),
'required_roles'=>$r->filled('required_roles') ? array_values(array_filter(array_map('trim', explode(',', $r->required_roles)))) : null,
'required_permissions'=>$r->filled('required_permissions') ? array_values(array_filter(array_map('trim', explode(',', $r->required_permissions)))) : null,
]);
return redirect()->route('admin.systems.menu.index')->with('ok','Voce aggiunta');
}
public function edit(MenuItem $item) {
$menu = Menu::where('name','admin_main')->first();
$parents = $menu?->items()->where('id','!=',$item->id)->orderBy('label')->get();
return view('admin.systems.menu.edit', compact('item','parents'));
}
public function update(Request $r, MenuItem $item) {
$r->validate([
'label'=>'required|string|max:100',
'parent_id'=>'nullable|integer',
'route_name'=>'nullable|string|max:120',
'url'=>'nullable|url',
'order_index'=>'nullable|integer',
]);
$item->update([
'parent_id'=>$r->parent_id,
'label'=>$r->label,
'route_name'=>$r->route_name ?: null,
'url'=>$r->url ?: null,
'icon'=>$r->icon,
'description'=>$r->description,
'order_index'=>$r->order_index ?? 0,
'is_visible'=>$r->boolean('is_visible', true),
'required_roles'=>$r->filled('required_roles') ? array_values(array_filter(array_map('trim', explode(',', $r->required_roles)))) : null,
'required_permissions'=>$r->filled('required_permissions') ? array_values(array_filter(array_map('trim', explode(',', $r->required_permissions)))) : null,
]);
return redirect()->route('admin.systems.menu.index')->with('ok','Voce aggiornata');
}
public function destroy(MenuItem $item) {
$item->delete();
return back()->with('ok','Voce rimossa');
}
public function reorder(Request $r) {
foreach ($r->input('items',[]) as $row) {
\App\Models\MenuItem::whereKey($row['id'])->update(['order_index'=>$row['order_index']]);
}
return response()->json(['ok'=>true]);
}
}
`---
4) Models & Policy
4.1 Models
`php
<?php
// app/Models/Menu.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;class Menu extends Model {
protected $fillable = ['name','description','is_active'];
public function items() { return $this->hasMany(MenuItem::class); }
}
``php
<?php
// app/Models/MenuItem.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;class MenuItem extends Model {
protected $fillable = [
'menu_id','parent_id','label','route_name','url','icon','description',
'order_index','is_visible','required_roles','required_permissions'
];
protected $casts = ['required_roles'=>'array','required_permissions'=>'array'];
public function parent(){ return $this->belongsTo(MenuItem::class,'parent_id'); }
public function children(){ return $this->hasMany(MenuItem::class,'parent_id'); }
public function menu(){ return $this->belongsTo(Menu::class); }
}
`4.2 Policy (opzionale per gestione menu)
`php
<?php
// app/Policies/MenuPolicy.php
namespace App\Policies;
use App\Models\User;
use App\Models\MenuItem;class MenuPolicy
{
public function manage(User $user) {
return $user->hasRole('super-admin');
}
public function view(User $user, MenuItem $item) {
if ($user->hasRole('super-admin')) return true;
$activeRole = session('active_role');
if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false;
if ($item->required_permissions) {
foreach ($item->required_permissions as $perm) {
if (!$user->can($perm)) return false;
}
}
return true;
}
}
`---
5) Service & Helpers (menu rendering/attivi)
5.1 Service
`php
<?php
// app/Services/MenuService.php
namespace App\Services;
use App\Models\Menu;
use App\Models\MenuItem;
use App\Models\User;class MenuService {
public function forUserMenu(string $menuName, ?User $user): array {
$menu = Menu::where('name',$menuName)->first();
if (!$menu) return [];
$items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id');
$activeRole = session('active_role');
$filter = function(MenuItem $item) use ($user,$activeRole) {
if (!$item->is_visible) return false;
if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false;
if ($item->required_permissions) {
foreach ($item->required_permissions as $perm) {
if (!$user || !$user->can($perm)) return false;
}
}
return true;
};
$build = function($parentId) use (&$build, $items, $filter) {
return ($items[$parentId] ?? collect())->filter($filter)->map(function($i) use (&$build) {
return [
'id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'icon'=>$i->icon,
'children'=>$build($i->id)->values()->all()
];
});
};
return $build(null)->values()->all();
}
}
`5.2 Helper Blade (attivo)
`php
<?php
// app/helpers.php
if (!function_exists('menu_is_active')) {
function menu_is_active(?string $routeName): bool {
if (!$routeName) return false;
return request()->routeIs($routeName) || request()->routeIs($routeName.'.*');
}
}
`composer.json (autoload):
`json
"autoload": {
"psr-4": {
"App\\": "app/"
},
"files": [
"app/helpers.php"
]
}
`---
6) Views Blade (layout + sezioni)
6.1 Layout admin
`blade
{{-- resources/views/layouts/admin.blade.php --}}
@php($menu = app(\App\Services\MenuService::class)->forUserMenu('admin_main', auth()->user()))
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title','sartUP Admin')</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
<header class="h-14 shadow flex items-center px-4 bg-white">
<nav class="flex gap-4">
@foreach($menu as $item)
<a class="font-medium {{ menu_is_active($item['route_name']) ? 'text-blue-600' : '' }}"
href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}">
{{ $item['label'] }}
</a>
@endforeach
@role('super-admin')
<a class="font-medium {{ request()->is('admin/systems*') ? 'text-blue-600' : '' }}"
href="{{ route('admin.systems.menu.index') }}">Servizio</a>
@endrole
</nav>
<div class="ml-auto flex items-center gap-2">
@if(session('active_role'))
<span class="text-xs px-2 py-1 bg-gray-200 rounded">Ruolo: {{ session('active_role') }}</span>
@endif
<form method="POST" action="{{ route('logout') }}">@csrf<button>Logout</button></form>
</div>
</header>
<div class="flex">
<aside class="w-64 bg-white border-r min-h-[calc(100vh-3.5rem)] p-3">
@php($first = $menu[0] ?? null)
@if($first && count($first['children']))
@foreach($first['children'] as $child)
<div class="mb-3">
<div class="font-semibold">{{ $child['label'] }}</div>
@if(count($child['children']))
<ul class="ml-3 list-disc">
@foreach($child['children'] as $sub)
<li>
<a href="{{ $sub['route_name'] ? route($sub['route_name']) : ($sub['url'] ?? '#') }}">
{{ $sub['label'] }}
</a>
</li>
@endforeach
</ul>
@endif
</div>
@endforeach
@endif
</aside>
<main class="flex-1 p-6">
@if(session('ok')) <div class="mb-4 p-2 bg-green-100">{{ session('ok') }}</div> @endif
@yield('content')
</main>
</div>
</body>
</html>
`6.2 Views “Servizio → Configurazione Menù”
`blade
{{-- resources/views/admin/systems/index.blade.php --}}
@extends('layouts.admin')
@section('title','Servizio')
@section('content')
<h1 class="text-xl font-semibold mb-4">Servizio</h1>
<ul class="list-disc ml-6">
<li><a href="{{ route('admin.systems.menu.index') }}">Configurazione menù</a></li>
</ul>
@endsection
``blade
{{-- resources/views/admin/systems/menu/index.blade.php --}}
@extends('layouts.admin')
@section('title','Configurazione menù')
@section('content')
<h1 class="text-xl font-semibold mb-4">Configurazione menù</h1> <a href="{{ route('admin.systems.menu.create') }}" class="underline">Nuova voce</a>
<table class="min-w-full bg-white shadow border mt-4">
<thead><tr>
<th class="p-2 text-left">Label</th>
<th class="p-2 text-left">Parent</th>
<th class="p-2 text-left">Route</th>
<th class="p-2 text-left">URL</th>
<th class="p-2 text-left">Roles</th>
<th class="p-2 text-left">Ordine</th>
<th class="p-2"></th>
</tr></thead>
<tbody>
@forelse($items as $i)
<tr>
<td class="p-2">{{ $i->label }}</td>
<td class="p-2">{{ $i->parent?->label ?? '—' }}</td>
<td class="p-2">{{ $i->route_name ?? '—' }}</td>
<td class="p-2">{{ $i->url ?? '—' }}</td>
<td class="p-2">{{ $i->required_roles ? implode(',', $i->required_roles) : '—' }}</td>
<td class="p-2">{{ $i->order_index }}</td>
<td class="p-2">
<a class="underline" href="{{ route('admin.systems.menu.edit',$i) }}">Modifica</a>
<form method="POST" action="{{ route('admin.systems.menu.destroy',$i) }}" class="inline">
@csrf @method('DELETE')
<button class="underline text-red-600" onclick="return confirm('Eliminare?')">Elimina</button>
</form>
</td>
</tr>
@empty
<tr><td class="p-2" colspan="7">Nessuna voce ancora.</td></tr>
@endforelse
</tbody>
</table>
@endsection
``blade
{{-- resources/views/admin/systems/menu/create.blade.php --}}
@extends('layouts.admin')
@section('title','Nuova voce menù')
@section('content')
<h1 class="text-xl font-semibold mb-4">Nuova voce</h1>
<form method="POST" action="{{ route('admin.systems.menu.store') }}">
@csrf
<div class="mb-2">
<label>Label</label>
<input class="border p-1 w-full" name="label" required>
</div>
<div class="mb-2">
<label>Parent</label>
<select class="border p-1 w-full" name="parent_id">
<option value="">—</option>
@foreach($parents as $p)
<option value="{{ $p->id }}">{{ $p->label }}</option>
@endforeach
</select>
</div>
<div class="mb-2">
<label>Route name</label>
<input class="border p-1 w-full" name="route_name">
</div>
<div class="mb-2">
<label>URL</label>
<input class="border p-1 w-full" name="url" placeholder="http(s)://…">
</div>
<div class="mb-2">
<label>Icon</label>
<input class="border p-1 w-full" name="icon">
</div>
<div class="mb-2">
<label>Descrizione</label>
<input class="border p-1 w-full" name="description">
</div>
<div class="mb-2">
<label>Ordine</label>
<input class="border p-1 w-full" name="order_index" type="number" value="0">
</div>
<div class="mb-2">
<label>Ruoli richiesti (comma separated)</label>
<input class="border p-1 w-full" name="required_roles" placeholder="admin,maintenance">
</div>
<div class="mb-2">
<label>Visibile?</label>
<input type="checkbox" name="is_visible" checked>
</div>
<button class="px-3 py-1 bg-blue-600 text-white rounded">Salva</button>
</form>
@endsection
``blade
{{-- resources/views/admin/systems/menu/edit.blade.php --}}
@extends('layouts.admin')
@section('title','Modifica voce menù')
@section('content')
<h1 class="text-xl font-semibold mb-4">Modifica voce</h1>
<form method="POST" action="{{ route('admin.systems.menu.update',$item) }}">
@csrf @method('PUT')
<div class="mb-2">
<label>Label</label>
<input class="border p-1 w-full" name="label" required value="{{ old('label',$item->label) }}">
</div>
<div class="mb-2">
<label>Parent</label>
<select class="border p-1 w-full" name="parent_id">
<option value="">—</option>
@foreach($parents as $p)
<option value="{{ $p->id }}" @selected($item->parent_id==$p->id)>{{ $p->label }}</option>
@endforeach
</select>
</div>
<div class="mb-2">
<label>Route name</label>
<input class="border p-1 w-full" name="route_name" value="{{ old('route_name',$item->route_name) }}">
</div>
<div class="mb-2">
<label>URL</label>
<input class="border p-1 w-full" name="url" value="{{ old('url',$item->url) }}">
</div>
<div class="mb-2">
<label>Icon</label>
<input class="border p-1 w-full" name="icon" value="{{ old('icon',$item->icon) }}">
</div>
<div class="mb-2">
<label>Descrizione</label>
<input class="border p-1 w-full" name="description" value="{{ old('description',$item->description) }}">
</div>
<div class="mb-2">
<label>Ordine</label>
<input class="border p-1 w-full" name="order_index" type="number" value="{{ old('order_index',$item->order_index) }}">
</div>
<div class="mb-2">
<label>Ruoli richiesti (comma separated)</label>
<input class="border p-1 w-full" name="required_roles" value="{{ old('required_roles', $item->required_roles ? implode(',',$item->required_roles) : '') }}">
</div>
<div class="mb-2">
<label>Visibile?</label>
<input type="checkbox" name="is_visible" @checked($item->is_visible)>
</div>
<button class="px-3 py-1 bg-blue-600 text-white rounded">Aggiorna</button>
</form>
@endsection
`---
7) Menu dinamico (seed iniziale con “Servizio”)
`php
$service = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Servizio'
],[
'icon'=>'lucide-wrench',
'order_index'=>100,
'required_roles'=>json_encode(['super-admin'])
]);\App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$service->id,'label'=>'Configurazione menù'
],[
'route_name'=>'admin.systems.menu.index',
'order_index'=>1,
'required_roles'=>json_encode(['super-admin'])
]);
`---
8) Sicurezza accessi
Gate::before + middleware role:super-admin sulle rotte “Servizio”.
EnsureActiveRole garantisce selezione ruolo.
MenuService filtra per required_roles e required_permissions.
---
9) Criteri di accettazione
---
10) TODO per Cursor
routes/auth.php e routes/admin.php + includerli in RouteServiceProvider
MenuService + helper menu_is_active()
layouts/admin.blade.php + views sistemi/menu (index/create/edit)
EnsureActiveRole registrato in Kernel
AuthServiceProvider`
Analisi Codice
Blocco 1
app/
Http/
Controllers/
Admin/
Dashboard/
DashboardController.php
I40/
HomeController.php
Machines/
MachinesController.php
Systems/ # "Servizio" (super-admin only)
Menu/
MenuController.php
Users/
UsersController.php
Roles/
RolesController.php
Middleware/
EnsureActiveRole.php
Models/
Menu.php
MenuItem.php
Policies/
MenuPolicy.php
app/
Services/
MenuService.php
resources/
views/
layouts/
admin.blade.php
auth.blade.php
auth/
login.blade.php
select-role.blade.php
forgot.blade.php
reset.blade.php
admin/
dashboard/index.blade.php
i40/home.blade.php
i40/machines/connected.blade.php
systems/ # Servizio (Super Admin)
index.blade.php
menu/
index.blade.php # lista voci
create.blade.php
edit.blade.php
routes/
admin.php
auth.php
database/
migrations/
2025_xx_xx_create_menus_table.php
2025_xx_xx_create_menu_items_table.php
seeders/
RolesSeeder.php
SuperAdminSeeder.php
MenuSeeder.php
Blocco 2 php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\ResetPasswordController;
use App\Http\Controllers\Auth\RoleSelectorController;
Route::middleware('guest')->group(function () {
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('/login', [LoginController::class, 'login'])->name('login.post');
Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update');
});
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');
Route::middleware('auth')->group(function () {
Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select');
Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set');
});
Blocco 3 php
<?php
use Illuminate\Support\Facades\Route;
Route::middleware(['auth','active.role'])->prefix('admin')->name('admin.')->group(function () {
// Dashboard
Route::get('/', [\App\Http\Controllers\Admin\Dashboard\DashboardController::class,'index'])
->name('dashboard');
// Industria 4.0
Route::prefix('i40')->name('i40.')->group(function() {
Route::get('/', [\App\Http\Controllers\Admin\I40\HomeController::class,'index'])->name('home');
Route::get('/machines/connected', [\App\Http\Controllers\Admin\I40\Machines\MachinesController::class,'connected'])
->name('machines.connected');
});
// Servizio (Super Admin menu)
Route::prefix('systems')->name('systems.')->middleware('role:super-admin')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('home');
// Configurazione menù (prima voce)
Route::prefix('menu')->name('menu.')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('index');
Route::get('/create', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'create'])->name('create');
Route::post('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'store'])->name('store');
Route::get('/{item}/edit', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'edit'])->name('edit');
Route::put('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'update'])->name('update');
Route::delete('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'destroy'])->name('destroy');
Route::post('/reorder', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'reorder'])->name('reorder');
});
});
});
Blocco 4 php
<?php
namespace App\Http\Controllers\Admin\Dashboard;
use App\Http\Controllers\Controller;
class DashboardController extends Controller
{
public function index() {
return view('admin.dashboard.index');
}
}
Blocco 5 php
<?php
namespace App\Http\Controllers\Admin\I40\Machines;
use App\Http\Controllers\Controller;
class MachinesController extends Controller
{
public function connected() {
return view('admin.i40.machines.connected');
}
}
Blocco 6 php
<?php
namespace App\Http\Controllers\Admin\Systems\Menu;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Menu;
use App\Models\MenuItem;
class MenuController extends Controller
{
public function index() {
$menu = Menu::where('name','admin_main')->first();
$items = $menu ? $menu->items()->orderBy('order_index')->get() : collect();
return view('admin.systems.menu.index', compact('menu','items'));
}
public function create() {
$menu = Menu::where('name','admin_main')->first();
$parents = $menu?->items()->orderBy('label')->get();
return view('admin.systems.menu.create', compact('menu','parents'));
}
public function store(Request $r) {
$r->validate([
'label'=>'required|string|max:100',
'parent_id'=>'nullable|integer',
'route_name'=>'nullable|string|max:120',
'url'=>'nullable|url',
'order_index'=>'nullable|integer',
]);
$menu = Menu::firstOrCreate(['name'=>'admin_main']);
MenuItem::create([
'menu_id'=>$menu->id,
'parent_id'=>$r->parent_id,
'label'=>$r->label,
'route_name'=>$r->route_name ?: null,
'url'=>$r->url ?: null,
'icon'=>$r->icon,
'description'=>$r->description,
'order_index'=>$r->order_index ?? 0,
'is_visible'=>$r->boolean('is_visible', true),
'required_roles'=>$r->filled('required_roles') ? array_values(array_filter(array_map('trim', explode(',', $r->required_roles)))) : null,
'required_permissions'=>$r->filled('required_permissions') ? array_values(array_filter(array_map('trim', explode(',', $r->required_permissions)))) : null,
]);
return redirect()->route('admin.systems.menu.index')->with('ok','Voce aggiunta');
}
public function edit(MenuItem $item) {
$menu = Menu::where('name','admin_main')->first();
$parents = $menu?->items()->where('id','!=',$item->id)->orderBy('label')->get();
return view('admin.systems.menu.edit', compact('item','parents'));
}
public function update(Request $r, MenuItem $item) {
$r->validate([
'label'=>'required|string|max:100',
'parent_id'=>'nullable|integer',
'route_name'=>'nullable|string|max:120',
'url'=>'nullable|url',
'order_index'=>'nullable|integer',
]);
$item->update([
'parent_id'=>$r->parent_id,
'label'=>$r->label,
'route_name'=>$r->route_name ?: null,
'url'=>$r->url ?: null,
'icon'=>$r->icon,
'description'=>$r->description,
'order_index'=>$r->order_index ?? 0,
'is_visible'=>$r->boolean('is_visible', true),
'required_roles'=>$r->filled('required_roles') ? array_values(array_filter(array_map('trim', explode(',', $r->required_roles)))) : null,
'required_permissions'=>$r->filled('required_permissions') ? array_values(array_filter(array_map('trim', explode(',', $r->required_permissions)))) : null,
]);
return redirect()->route('admin.systems.menu.index')->with('ok','Voce aggiornata');
}
public function destroy(MenuItem $item) {
$item->delete();
return back()->with('ok','Voce rimossa');
}
public function reorder(Request $r) {
foreach ($r->input('items',[]) as $row) {
\App\Models\MenuItem::whereKey($row['id'])->update(['order_index'=>$row['order_index']]);
}
return response()->json(['ok'=>true]);
}
}
Blocco 7 php
<?php
// app/Models/Menu.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Menu extends Model {
protected $fillable = ['name','description','is_active'];
public function items() { return $this->hasMany(MenuItem::class); }
}
Blocco 8 php
<?php
// app/Models/MenuItem.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class MenuItem extends Model {
protected $fillable = [
'menu_id','parent_id','label','route_name','url','icon','description',
'order_index','is_visible','required_roles','required_permissions'
];
protected $casts = ['required_roles'=>'array','required_permissions'=>'array'];
public function parent(){ return $this->belongsTo(MenuItem::class,'parent_id'); }
public function children(){ return $this->hasMany(MenuItem::class,'parent_id'); }
public function menu(){ return $this->belongsTo(Menu::class); }
}
Blocco 9 php
<?php
// app/Policies/MenuPolicy.php
namespace App\Policies;
use App\Models\User;
use App\Models\MenuItem;
class MenuPolicy
{
public function manage(User $user) {
return $user->hasRole('super-admin');
}
public function view(User $user, MenuItem $item) {
if ($user->hasRole('super-admin')) return true;
$activeRole = session('active_role');
if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false;
if ($item->required_permissions) {
foreach ($item->required_permissions as $perm) {
if (!$user->can($perm)) return false;
}
}
return true;
}
}
Blocco 10 php
<?php
// app/Services/MenuService.php
namespace App\Services;
use App\Models\Menu;
use App\Models\MenuItem;
use App\Models\User;
class MenuService {
public function forUserMenu(string $menuName, ?User $user): array {
$menu = Menu::where('name',$menuName)->first();
if (!$menu) return [];
$items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id');
$activeRole = session('active_role');
$filter = function(MenuItem $item) use ($user,$activeRole) {
if (!$item->is_visible) return false;
if ($item->required_roles && $activeRole && !in_array($activeRole, $item->required_roles)) return false;
if ($item->required_permissions) {
foreach ($item->required_permissions as $perm) {
if (!$user || !$user->can($perm)) return false;
}
}
return true;
};
$build = function($parentId) use (&$build, $items, $filter) {
return ($items[$parentId] ?? collect())->filter($filter)->map(function($i) use (&$build) {
return [
'id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'icon'=>$i->icon,
'children'=>$build($i->id)->values()->all()
];
});
};
return $build(null)->values()->all();
}
}
Blocco 11 php
<?php
// app/helpers.php
if (!function_exists('menu_is_active')) {
function menu_is_active(?string $routeName): bool {
if (!$routeName) return false;
return request()->routeIs($routeName) || request()->routeIs($routeName.'.*');
}
}
Blocco 12 json
"autoload": {
"psr-4": {
"App\\": "app/"
},
"files": [
"app/helpers.php"
]
}
Blocco 13 blade
{{-- resources/views/layouts/admin.blade.php --}}
@php($menu = app(\App\Services\MenuService::class)->forUserMenu('admin_main', auth()->user()))
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title','sartUP Admin')</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
<header class="h-14 shadow flex items-center px-4 bg-white">
<nav class="flex gap-4">
@foreach($menu as $item)
<a class="font-medium {{ menu_is_active($item['route_name']) ? 'text-blue-600' : '' }}"
href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}">
{{ $item['label'] }}
</a>
@endforeach
@role('super-admin')
<a class="font-medium {{ request()->is('admin/systems*') ? 'text-blue-600' : '' }}"
href="{{ route('admin.systems.menu.index') }}">Servizio</a>
@endrole
</nav>
<div class="ml-auto flex items-center gap-2">
@if(session('active_role'))
<span class="text-xs px-2 py-1 bg-gray-200 rounded">Ruolo: {{ session('active_role') }}</span>
@endif
<form method="POST" action="{{ route('logout') }}">@csrf<button>Logout</button></form>
</div>
</header>
<div class="flex">
<aside class="w-64 bg-white border-r min-h-[calc(100vh-3.5rem)] p-3">
@php($first = $menu[0] ?? null)
@if($first && count($first['children']))
@foreach($first['children'] as $child)
<div class="mb-3">
<div class="font-semibold">{{ $child['label'] }}</div>
@if(count($child['children']))
<ul class="ml-3 list-disc">
@foreach($child['children'] as $sub)
<li>
<a href="{{ $sub['route_name'] ? route($sub['route_name']) : ($sub['url'] ?? '#') }}">
{{ $sub['label'] }}
</a>
</li>
@endforeach
</ul>
@endif
</div>
@endforeach
@endif
</aside>
<main class="flex-1 p-6">
@if(session('ok')) <div class="mb-4 p-2 bg-green-100">{{ session('ok') }}</div> @endif
@yield('content')
</main>
</div>
</body>
</html>
Blocco 14 blade
{{-- resources/views/admin/systems/index.blade.php --}}
@extends('layouts.admin')
@section('title','Servizio')
@section('content')
<h1 class="text-xl font-semibold mb-4">Servizio</h1>
<ul class="list-disc ml-6">
<li><a href="{{ route('admin.systems.menu.index') }}">Configurazione menù</a></li>
</ul>
@endsection
Blocco 15 blade
{{-- resources/views/admin/systems/menu/index.blade.php --}}
@extends('layouts.admin')
@section('title','Configurazione menù')
@section('content')
<h1 class="text-xl font-semibold mb-4">Configurazione menù</h1>
<a href="{{ route('admin.systems.menu.create') }}" class="underline">Nuova voce</a>
<table class="min-w-full bg-white shadow border mt-4">
<thead><tr>
<th class="p-2 text-left">Label</th>
<th class="p-2 text-left">Parent</th>
<th class="p-2 text-left">Route</th>
<th class="p-2 text-left">URL</th>
<th class="p-2 text-left">Roles</th>
<th class="p-2 text-left">Ordine</th>
<th class="p-2"></th>
</tr></thead>
<tbody>
@forelse($items as $i)
<tr>
<td class="p-2">{{ $i->label }}</td>
<td class="p-2">{{ $i->parent?->label ?? '—' }}</td>
<td class="p-2">{{ $i->route_name ?? '—' }}</td>
<td class="p-2">{{ $i->url ?? '—' }}</td>
<td class="p-2">{{ $i->required_roles ? implode(',', $i->required_roles) : '—' }}</td>
<td class="p-2">{{ $i->order_index }}</td>
<td class="p-2">
<a class="underline" href="{{ route('admin.systems.menu.edit',$i) }}">Modifica</a>
<form method="POST" action="{{ route('admin.systems.menu.destroy',$i) }}" class="inline">
@csrf @method('DELETE')
<button class="underline text-red-600" onclick="return confirm('Eliminare?')">Elimina</button>
</form>
</td>
</tr>
@empty
<tr><td class="p-2" colspan="7">Nessuna voce ancora.</td></tr>
@endforelse
</tbody>
</table>
@endsection
Blocco 16 blade
{{-- resources/views/admin/systems/menu/create.blade.php --}}
@extends('layouts.admin')
@section('title','Nuova voce menù')
@section('content')
<h1 class="text-xl font-semibold mb-4">Nuova voce</h1>
<form method="POST" action="{{ route('admin.systems.menu.store') }}">
@csrf
<div class="mb-2">
<label>Label</label>
<input class="border p-1 w-full" name="label" required>
</div>
<div class="mb-2">
<label>Parent</label>
<select class="border p-1 w-full" name="parent_id">
<option value="">—</option>
@foreach($parents as $p)
<option value="{{ $p->id }}">{{ $p->label }}</option>
@endforeach
</select>
</div>
<div class="mb-2">
<label>Route name</label>
<input class="border p-1 w-full" name="route_name">
</div>
<div class="mb-2">
<label>URL</label>
<input class="border p-1 w-full" name="url" placeholder="http(s)://…">
</div>
<div class="mb-2">
<label>Icon</label>
<input class="border p-1 w-full" name="icon">
</div>
<div class="mb-2">
<label>Descrizione</label>
<input class="border p-1 w-full" name="description">
</div>
<div class="mb-2">
<label>Ordine</label>
<input class="border p-1 w-full" name="order_index" type="number" value="0">
</div>
<div class="mb-2">
<label>Ruoli richiesti (comma separated)</label>
<input class="border p-1 w-full" name="required_roles" placeholder="admin,maintenance">
</div>
<div class="mb-2">
<label>Visibile?</label>
<input type="checkbox" name="is_visible" checked>
</div>
<button class="px-3 py-1 bg-blue-600 text-white rounded">Salva</button>
</form>
@endsection
Blocco 17 blade
{{-- resources/views/admin/systems/menu/edit.blade.php --}}
@extends('layouts.admin')
@section('title','Modifica voce menù')
@section('content')
<h1 class="text-xl font-semibold mb-4">Modifica voce</h1>
<form method="POST" action="{{ route('admin.systems.menu.update',$item) }}">
@csrf @method('PUT')
<div class="mb-2">
<label>Label</label>
<input class="border p-1 w-full" name="label" required value="{{ old('label',$item->label) }}">
</div>
<div class="mb-2">
<label>Parent</label>
<select class="border p-1 w-full" name="parent_id">
<option value="">—</option>
@foreach($parents as $p)
<option value="{{ $p->id }}" @selected($item->parent_id==$p->id)>{{ $p->label }}</option>
@endforeach
</select>
</div>
<div class="mb-2">
<label>Route name</label>
<input class="border p-1 w-full" name="route_name" value="{{ old('route_name',$item->route_name) }}">
</div>
<div class="mb-2">
<label>URL</label>
<input class="border p-1 w-full" name="url" value="{{ old('url',$item->url) }}">
</div>
<div class="mb-2">
<label>Icon</label>
<input class="border p-1 w-full" name="icon" value="{{ old('icon',$item->icon) }}">
</div>
<div class="mb-2">
<label>Descrizione</label>
<input class="border p-1 w-full" name="description" value="{{ old('description',$item->description) }}">
</div>
<div class="mb-2">
<label>Ordine</label>
<input class="border p-1 w-full" name="order_index" type="number" value="{{ old('order_index',$item->order_index) }}">
</div>
<div class="mb-2">
<label>Ruoli richiesti (comma separated)</label>
<input class="border p-1 w-full" name="required_roles" value="{{ old('required_roles', $item->required_roles ? implode(',',$item->required_roles) : '') }}">
</div>
<div class="mb-2">
<label>Visibile?</label>
<input type="checkbox" name="is_visible" @checked($item->is_visible)>
</div>
<button class="px-3 py-1 bg-blue-600 text-white rounded">Aggiorna</button>
</form>
@endsection
Blocco 18 php
$service = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Servizio'
],[
'icon'=>'lucide-wrench',
'order_index'=>100,
'required_roles'=>json_encode(['super-admin'])
]);
\App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$service->id,'label'=>'Configurazione menù'
],[
'route_name'=>'admin.systems.menu.index',
'order_index'=>1,
'required_roles'=>json_encode(['super-admin'])
]);